/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 *  Copyright 2008 Global Sign In
 *  Copyright 2009 Fabrice Colin
 *
 *  The MySQL FLOSS License Exception allows us to license this code under
 *  the LGPL even though MySQL is under the GPL.
 * 
 *  This code is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
#include <algorithm>
#include <iostream>
#include <sstream>

#include "MySQLBase.h"

using std::clog;
using std::endl;
using std::string;
using std::stringstream;
using std::vector;

MySQLRow::MySQLRow(MYSQL_ROW row, int nColumns) :
	m_row(row),
	m_nColumns(nColumns)
{
}

MySQLRow::~MySQLRow()
{
}

int MySQLRow::getColumnsCount(void) const
{
	return m_nColumns;
}

string MySQLRow::getColumn(int nColumn) const
{
	if ((nColumn < m_nColumns) &&
		(m_row[nColumn] != NULL))
	{
		return m_row[nColumn];
	}

	return "";
}

MySQLResults::MySQLResults(MYSQL_RES *results) :
	m_results(results),
	m_nRows(mysql_num_rows(results)),
	m_nColumns(mysql_num_fields(results)),
	m_nCurrentRow(0)
{
	// Check we actually have results
	if ((m_results == NULL) ||
		(m_nRows <= 0))
	{
		m_nRows = m_nCurrentRow = 0;
		m_nColumns = 0;
	}
}

MySQLResults::~MySQLResults()
{
	mysql_free_result(m_results);
}

unsigned long MySQLResults::getRowsCount(void)
{
	return (unsigned long)m_nRows;
}

bool MySQLResults::hasMoreRows(void)
{
	if ((m_nCurrentRow >= 0) &&
		(m_nCurrentRow < m_nRows))
	{
		return true;
	}

	return false;
}

string MySQLResults::getColumnName(unsigned int nColumn)
{
	if (nColumn < m_nColumns)
	{
		MYSQL_FIELD *pField = mysql_fetch_field_direct(m_results, nColumn);
		if ((pField != NULL) &&
			(pField->name != NULL))
		{
			return pField->name;
		}
	}

	return "";
}

MySQLRow *MySQLResults::nextRow(void)
{
	if (m_nCurrentRow >= m_nRows)
	{
		return NULL;
	}

	MYSQL_ROW row = mysql_fetch_row(m_results);
	if (row == NULL)
	{
		return NULL;
	}
	++m_nCurrentRow;

	return new MySQLRow(row, m_nColumns);
}

bool MySQLResults::rewind(void)
{
	m_nCurrentRow = 0;

	return true;
}

MySQLBase::MySQLBase(const string &hostName, const string &databaseName,
	const string &userName, const string &password) :
	m_hostName(hostName),
	m_databaseName(databaseName),
	m_userName(userName),
	m_password(password),
	m_isOpen(false)
{
	pthread_mutex_init(&m_mutex, 0);
	open();
}

MySQLBase::~MySQLBase()
{
	close();
	pthread_mutex_destroy(&m_mutex);
}

bool MySQLBase::isOpen(void) const
{
	return m_isOpen;
}

void MySQLBase::open(void)
{
	// Initialize
	if (mysql_init(&m_database) == NULL)
	{
		clog << "Couldn't initialize MySQL API" << endl;
		return;
	}

	// Connect to the database
	if (mysql_real_connect(&m_database, m_hostName.c_str(),
		m_userName.c_str(), m_password.c_str(), m_databaseName.c_str(),
		0, "/var/lib/mysql/mysql.sock", 0) == NULL)
	{
		clog << "MySQL error " << mysql_errno(&m_database)
			<< ": " << mysql_error(&m_database) << endl;
		return;
	}

	m_isOpen = true;
}

void MySQLBase::close(void)
{
	m_isOpen = false;
	mysql_close(&m_database);
}

string MySQLBase::getUniversalUniqueId(void)
{
	MySQLResults *pResults = executeStatement("SELECT UUID();");
	if (pResults == NULL)
	{
		return "";
	}

	// There should only be one row
	MySQLRow *pRow = pResults->nextRow();
	if (pRow == NULL)
	{
		delete pResults;
		return "";
	}

	string uuid(pRow->getColumn(0));

	delete pRow;
	delete pResults;

	return uuid;
}

string MySQLBase::escapeString(const string &text)
{
	if ((text.empty() == true) ||
		(m_isOpen == false))
	{
		return "";
	}

	string modText(text);

	// Double up % wildcards so that they are not mistaken for format specifiers in executeStatement()
	string::size_type percentPos = modText.find('%');
	while (percentPos != string::npos)
	{
		modText.replace(percentPos, 1, "%%");

		// Next
		if (percentPos + 2 < modText.length())
		{
			percentPos = modText.find('%', percentPos + 2);
		}
		else
		{
			break;
		}
	}

	char *pEscapedText = new char[(modText.length() * 2) + 1];

	unsigned long escapedLen = mysql_real_escape_string(&m_database,
		pEscapedText, modText.c_str(), modText.length());

	string escapedText(pEscapedText, escapedLen);

	delete[] pEscapedText;

	return escapedText;
}

bool MySQLBase::executeSimpleStatement(const string &sql, string &insertAutoId)
{
	bool success = false;

	if ((sql.empty() == true) ||
		(m_isOpen == false))
	{
		return false;
	}

	insertAutoId.clear();

	pthread_mutex_lock(&m_mutex);
	if (mysql_query(&m_database, sql.c_str()))
	{
		unsigned int errorCode = mysql_errno(&m_database);

		clog << "SQL statement <" << sql << "> failed with error "
			<< errorCode << ": " << mysql_error(&m_database) << endl;
		pthread_mutex_unlock(&m_mutex);

		return false;
	}

	MYSQL_RES *pResult = mysql_store_result(&m_database);
	if (pResult != NULL)
	{
		success = true;

		mysql_free_result(pResult);
	}
	else if (mysql_field_count(&m_database) == 0)
	{
		stringstream idStream;

		// The query didn't have to return results, ie it wasn't a SELECT
		success = true;

		// Get the last auto generated Id, if any
		idStream << mysql_insert_id(&m_database);
		insertAutoId = idStream.str();
	}
	pthread_mutex_unlock(&m_mutex);

	return success;
}

MySQLResults *MySQLBase::executeStatement(const char *sqlFormat, ...)
{
	MySQLResults *pResults = NULL;
	char stringBuff[2048];
	va_list ap;

	if ((sqlFormat == NULL) ||
		(m_isOpen == false))
	{
		return NULL;
	}

	va_start(ap, sqlFormat);
	int numChars = vsnprintf(stringBuff, 2048, sqlFormat, ap);
	if (numChars <= 0)
	{
#ifdef DEBUG
		clog << "MySQLBase::executeStatement: couldn't format statement" << endl;
#endif
		return NULL;
	}
	if (numChars >= 2048)
	{
		// Not enough space
#ifdef DEBUG
		clog << "MySQLBase::executeStatement: not enough space (" << numChars << ")" << endl;
#endif
		return NULL;
	}
	stringBuff[numChars] = '\0';
	va_end(ap);

	pthread_mutex_lock(&m_mutex);
	if (mysql_query(&m_database, stringBuff))
	{
		unsigned int errorCode = mysql_errno(&m_database);

		clog << "SQL statement <" << stringBuff << "> failed with error "
			<< errorCode << ": " << mysql_error(&m_database) << endl;
		pthread_mutex_unlock(&m_mutex);

		return NULL;
	}

	MYSQL_RES *pResult = mysql_store_result(&m_database);
	if (pResult != NULL)
	{
		pResults = new MySQLResults(pResult);
	}
	pthread_mutex_unlock(&m_mutex);

	return pResults;
}

MySQLResults *MySQLBase::executeStatement(const string &sqlFormat,
	unsigned long min, unsigned long max)
{
	if (sqlFormat.empty() == true)
	{
		return NULL;
	}

	stringstream paginationStatement;
	paginationStatement << sqlFormat;
	paginationStatement << " LIMIT ";
	paginationStatement << max - min;
	paginationStatement << " OFFSET ";
	paginationStatement << min;
	paginationStatement << ";";

	// Call overload
	return executeStatement(paginationStatement.str().c_str());
}

